package digitRecognitionProblem.learnWeights;

import genetic_algorithm.Chromosome;
import genetic_algorithm.Crossover;
import genetic_algorithm.Mutation;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;

import mlp.Layer;
import mlp.Mlp;
import utils.RandomGenerator;

/**
 * Crossover phase implementation. There are 3 crossover operations,
 * which are chosen at probability which can be set in the main class.
 * Once a crossover operator is chosen, it is applied on each layer separately
 */
public class LWcrossover implements Crossover {

	// must exist SINGLE < DOUBLE < UNIFORM and SINGLE + DOUBLE + UNIFORM = 1
	public static double SINGLE_MODE_PROB;
	public static double DOUBLE_MODE_PROB;
	public static double UNIFORM_MODE_PROB;
	
	@Override
	public Entry<Chromosome, Chromosome> mate(
			Entry<Chromosome, Chromosome> parents, double crossoverRate,
			Mutation mutator, double mutationRate) {

		// skip crossover in chance of 1-crossover_rate
		if ((RandomGenerator.nextInt(1000) / 1000.0) > crossoverRate) {
			
			// mutate the parents
			List<Chromosome> parentList = new LinkedList<Chromosome>();
			parentList.add(parents.getKey());
			parentList.add(parents.getValue());
			mutator.mutate(parentList, mutationRate);
			
			return new AbstractMap.SimpleEntry<Chromosome, Chromosome>(
					parentList.get(0), parentList.get(1));
		}
		
		// choose randomly the crossover mode
		List<Chromosome> offsprings = null;
		double chance = RandomGenerator.nextDouble();
		if (chance <= SINGLE_MODE_PROB) {
			offsprings = singlePoint(parents.getKey(), parents.getValue());
		} else if (chance <= DOUBLE_MODE_PROB) {
			offsprings = doublePoint(parents.getKey(), parents.getValue());
		} else {
			offsprings = uniform(parents.getKey(), parents.getValue());
		}
		
		// mutate the offsprings
		mutator.mutate(offsprings, mutationRate);

		// return resulting offsprings
		return new AbstractMap.SimpleEntry<Chromosome, Chromosome>(
				offsprings.get(0), offsprings.get(1));		
	}

	private List<Chromosome> singlePoint(Chromosome father, Chromosome mother) {

		// extract number of layers		
		LWchromosome fatherMlp = (LWchromosome) father;
		LWchromosome motherMlp = (LWchromosome) mother;
		int numLayers = fatherMlp.getAllValues().size();
		
		// initialize values list for offspring chromosomes
		ArrayList<Layer> firstOffspringVals = new ArrayList<Layer>(numLayers);
		ArrayList<Layer> secondOffspringVals = new ArrayList<Layer>(numLayers);
		
		// construct each layers of the offsprings
		for (int i = 0 ; i < numLayers ; ++i) {
			
			// randomly choose split index for current layer
			int numNeurons = fatherMlp.getValue(i).size();
			int splitIndex = RandomGenerator.nextInt(numNeurons);
			
			// extract prefix and postfix of chromosomes' values
			List<float[]> fatherPrefix = new ArrayList<float[]>(fatherMlp.getValue(i).getWeights(0, splitIndex));
			List<float[]> fatherPostfix = new ArrayList<float[]>(fatherMlp.getValue(i).getWeights(splitIndex,numNeurons));
			List<float[]> motherPrefix = new ArrayList<float[]>(motherMlp.getValue(i).getWeights(0, splitIndex));
			List<float[]> motherPostfix = new ArrayList<float[]>(motherMlp.getValue(i).getWeights(splitIndex,numNeurons));
			
			// concatenate father's prefix with mother's postfix
			List<float[]> weights = new ArrayList<float[]>(numNeurons);
			weights.addAll(fatherPrefix);
			weights.addAll(motherPostfix);
			firstOffspringVals.add(new Layer(weights));

			// concatenate mother's prefix with father's postfix
			weights.clear();
			weights.addAll(motherPrefix);
			weights.addAll(fatherPostfix);
			secondOffspringVals.add(new Layer(weights));
		}
				
		// generate chromosomes out of offsprings values
		LWchromosome firstOffspring = new LWchromosome(Mlp.createByLayers(firstOffspringVals));
		LWchromosome secondOffspring = new LWchromosome(Mlp.createByLayers(secondOffspringVals));
		
		// return resulting chromosomes
		List<Chromosome> offsprings = new LinkedList<Chromosome>();
		offsprings.add(firstOffspring);
		offsprings.add(secondOffspring);
		return offsprings;
	}
	
	private List<Chromosome> doublePoint(Chromosome father, Chromosome mother) {

		// extract number of layers		
		LWchromosome fatherMlp = (LWchromosome) father;
		LWchromosome motherMlp = (LWchromosome) mother;
		int numLayers = fatherMlp.getAllValues().size();
		
		// initialize values list for offspring chromosomes
		ArrayList<Layer> firstOffspringVals = new ArrayList<Layer>(numLayers);
		ArrayList<Layer> secondOffspringVals = new ArrayList<Layer>(numLayers);

		// construct each layers of the offsprings
		for (int i = 0 ; i < numLayers ; ++i) {
			
			// randomly choose split indices
			int numNeurons = fatherMlp.getValue(i).size();
			int firstSplit = RandomGenerator.nextInt(numNeurons - 1);
			int secondSplit = RandomGenerator.nextInt(numNeurons - firstSplit) + firstSplit;
			
			// extract sublists of each chromosome's values			
			List<float[]> fatherFirst = new ArrayList<float[]>(fatherMlp.getValue(i).getWeights(0, firstSplit));
			List<float[]> fatherSecond = new ArrayList<float[]>(fatherMlp.getValue(i).getWeights(firstSplit, secondSplit));
			List<float[]> fatherThird = new ArrayList<float[]>(fatherMlp.getValue(i).getWeights(secondSplit, numNeurons));
			List<float[]> motherFirst = new ArrayList<float[]>(motherMlp.getValue(i).getWeights(0,firstSplit));
			List<float[]> motherSecond = new ArrayList<float[]>(motherMlp.getValue(i).getWeights(firstSplit, secondSplit));
			List<float[]> motherThird = new ArrayList<float[]>(motherMlp.getValue(i).getWeights(secondSplit, numNeurons));
			
			// concatenate father and mother's neurons alternately
			
			List<float[]> weights = new ArrayList<float[]>(numNeurons);
			weights.addAll(fatherFirst);
			weights.addAll(motherSecond);
			weights.addAll(fatherThird);
			firstOffspringVals.add(new Layer(weights));
			
			weights.clear();
			weights.addAll(motherFirst);
			weights.addAll(fatherSecond);
			weights.addAll(motherThird);
			secondOffspringVals.add(new Layer(weights));
		}
		
		// generate chromosomes out of offsprings values
		LWchromosome firstOffspring = new LWchromosome(Mlp.createByLayers(firstOffspringVals));
		LWchromosome secondOffspring = new LWchromosome(Mlp.createByLayers(secondOffspringVals));
		
		// return resulting chromosomes
		List<Chromosome> offsprings = new LinkedList<Chromosome>();
		offsprings.add(firstOffspring);
		offsprings.add(secondOffspring);
		return offsprings;
	}
	
	private List<Chromosome> uniform(Chromosome father, Chromosome mother) {

		// extract number of layers		
		LWchromosome fatherMlp = (LWchromosome) father;
		LWchromosome motherMlp = (LWchromosome) mother;
		int numLayers = fatherMlp.getAllValues().size();
		
		// initialize values list for offspring chromosomes
		ArrayList<Layer> firstOffspringVals = new ArrayList<Layer>(numLayers);
		ArrayList<Layer> secondOffspringVals = new ArrayList<Layer>(numLayers);
		
		// construct each layers of the offsprings
		for (int currLayer = 0 ; currLayer < numLayers ; ++currLayer) {

			// get input weights of all neurons of each parent's current layer
			int numNeurons = fatherMlp.getValue(currLayer).size();
			List<float[]> fatherWeights = fatherMlp.getValue(currLayer).getWeights(0, numNeurons);
			List<float[]> motherWeights = motherMlp.getValue(currLayer).getWeights(0, numNeurons);
			
			// choose randomly each neuron from the parents
			List<float[]> firstWeights = new ArrayList<float[]>(numNeurons);
			List<float[]> secondWeights = new ArrayList<float[]>(numNeurons);
			for (int currNeuron = 0 ; currNeuron < numNeurons ; ++currNeuron) {
				
				// choose randomly the parent to inherit its current value
				if (RandomGenerator.nextInt(2) == 0) {				
					firstWeights.add(fatherWeights.get(currNeuron));
					secondWeights.add(motherWeights.get(currNeuron));					
				} else {
					firstWeights.add(motherWeights.get(currNeuron));
					secondWeights.add(fatherWeights.get(currNeuron));
				}
			}

			// add current layer for each offspring
			firstOffspringVals.add(new Layer(firstWeights));
			secondOffspringVals.add(new Layer(secondWeights));
		}
		
		// generate chromosomes out of offsprings values
		LWchromosome firstOffspring = new LWchromosome(Mlp.createByLayers(firstOffspringVals));
		LWchromosome secondOffspring = new LWchromosome(Mlp.createByLayers(secondOffspringVals));
		
		// return resulting chromosomes
		List<Chromosome> offsprings = new LinkedList<Chromosome>();
		offsprings.add(firstOffspring);
		offsprings.add(secondOffspring);
		return offsprings;
	}
}
